﻿/************************************************************************

   Extended Silverlight Toolkit

   Copyright (C) 2010-2012 Xceed Software Inc.

   This program is provided to you under the terms of the Microsoft Public
   License (Ms-PL) as published at http://extendedsilverlight.codeplex.com/license

   Please purchase a commercial version of this toolkit if you are using
   it in a commercial product. The commercial versions support the project,
   and also include more features for each control, priority support,
   source code, updates, removed watermark, and a proprietary license.

   Visit http://xceed.com and follow @datagrid on Twitter.

  **********************************************************************/

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Windows.Controls;
using Xceed.Silverlight.Data;
using System.Diagnostics;

namespace Xceed.Silverlight.ListBox
{
  public sealed class SelectedItemsWrapper : IList, INotifyCollectionChanged, INotifyPropertyChanged
  {
    #region Constructor
    public SelectedItemsWrapper( ListBox listBox )
    {
      if( listBox == null )
        throw new NullReferenceException( "ListBox should not be null." );

      m_listBox = listBox;
      m_selectedItems = new List<object>( 128 );
      m_selectedItemsLookup = new HashSet<object>();
    }
    #endregion

    internal event EventHandler Completed;

    internal bool UpdateSelectedItems()
    {
      int oldSelectedItemsCount = m_selectedItems.Count;

      m_selectedItems.Clear();
      m_selectedItemsLookup.Clear();

      IList<SelectionRange> selectedRanges = m_listBox.SelectedRanges;

      int selectedRangesCount = selectedRanges.Count;

      // No selectedRanges
      if( selectedRangesCount == 0 )
      {
        // If selectedItems contained elements, notify
        if( oldSelectedItemsCount > 0 )
        {
          this.RaiseChangedEventsAndUpdateSelectionProperties(
             new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
        }

        return true;
      }

      // We return as if we succeeded the update
      // in async mode
      if( m_listBox.IsAsyncDataSource )
        return true;

      DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;
      DataRangeContext dataRangeContext = dataSourceWrapper.GetCurrentDataRangeContext();

      bool isSelectAll = false;

      for( int i = 0; i < selectedRangesCount; i++ )
      {
        SelectionRange selectionRange = selectedRanges[ i ];

        if( ( selectionRange.SelectFromStart )
          && ( selectionRange.SelectToEnd )
          && ( selectionRange.SelectionType == SelectionType.Selection ) )
        {
          isSelectAll = true;
          break;
        }
      }

      if( isSelectAll )
      {
        if( m_listBox.IsUnbound )
        {
          m_selectedItems.AddRange( m_listBox.Items.Cast<object>() );

          m_selectedItemsLookup = new HashSet<object>( m_selectedItems );

          this.RaiseChangedEventsAndUpdateSelectionProperties(
            new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );

          return true;
        }
        else
        {
          Debug.Assert( dataSourceWrapper != null );

          SelectedItemsAsyncResult selectedItemsResult = new SelectedItemsAsyncResult( selectedRanges,
            this.OnSelectedItemsAsyncResultCompleted,
            null,
            dataSourceWrapper );

          selectedItemsResult.Launch();

          return true;
        }
      }
      else
      {
        SelectedItemsAsyncResult selectedItemsResult = new SelectedItemsAsyncResult( selectedRanges,
          this.OnSelectedItemsAsyncResultCompleted,
          null,
          dataSourceWrapper );

        selectedItemsResult.Launch();

        return false;
      }
    }

    private void OnSelectedItemsAsyncResultCompleted( IAsyncResult asyncResult )
    {
      if( asyncResult == null )
        throw new ArgumentNullException( "asyncResult" );

      SelectedItemsAsyncResult selectedItemsAsyncResult = asyncResult as SelectedItemsAsyncResult;

      if( selectedItemsAsyncResult == null )
        throw new ArgumentException( "The specified asyncResult must be a SelectedItemAsyncResult.", "asyncResult" );

      m_selectedItems.AddRange( selectedItemsAsyncResult.SelectedItems );
      m_selectedItemsLookup = new HashSet<object>( m_selectedItems );

      this.RaiseCompleted();

      this.RaiseChangedEventsAndUpdateSelectionProperties(
        new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
    }

    private void RaiseCompleted()
    {
      if( this.Completed == null )
        return;

      this.Completed( this, EventArgs.Empty );
    }

    private void RaiseChangedEventsAndUpdateSelectionProperties( NotifyCollectionChangedEventArgs args )
    {
      this.RaisePropertyChanged( "Count" );
      this.RaisePropertyChanged( "Item[]" );
      this.RaiseCollectionChanged( args );
      this.UpdateSelectionPropertiesOnContainers();
    }

    private void UpdateSelectionPropertiesOnContainers()
    {
      if( m_listBox == null )
        return;

      PanelBase panelBase = m_listBox.Panel;

      if( panelBase == null )
        return;

      panelBase.UpdateSelectionPropertiesOnContainers();
    }

    private HashSet<object> m_selectedItemsLookup;
    private List<object> m_selectedItems;
    private ListBox m_listBox;

    #region INotifyCollectionChanged Members

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    private void RaiseCollectionChanged( NotifyCollectionChangedEventArgs args )
    {
      if( this.CollectionChanged == null )
        return;

      this.CollectionChanged( this, args );
    }

    #endregion

    #region INotifyPropertyChanged Members

    public event PropertyChangedEventHandler PropertyChanged;

    private void RaisePropertyChanged( string propertyName )
    {
      if( this.PropertyChanged == null )
        return;

      if( propertyName == null )
        propertyName = string.Empty;

      this.PropertyChanged( this, new PropertyChangedEventArgs( propertyName ) );
    }

    #endregion

    #region IList Members

    public int Add( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotSupportedException( "SelectedItems is read-only for asynchronous data sources." );

      if( m_listBox.SelectionMode == SelectionMode.Single )
        throw new InvalidOperationException( "Can only change SelectedItems collection in multiple selection modes. Use SelectedItem in single selection modes." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null." );

      if( m_selectedItemsLookup.Contains( value ) )
        return m_selectedItems.IndexOf( value );

      int addedIndex = m_selectedItems.Count;
      this.Insert( addedIndex, value );

      return addedIndex;
    }

    public void Clear()
    {
      int oldSelectedItemsCount = m_selectedItems.Count;

      m_selectedItems.Clear();
      m_selectedItemsLookup.Clear();

      // This check prevent an exception to be thrown if the changed notification came from
      // this collection.
      if( m_listBox.SelectedRanges.Count > 0 )
      {
        m_listBox.SelectedRanges.Clear();
      }

      if( oldSelectedItemsCount > 0 )
      {
        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
      }
    }

    public bool Contains( object value )
    {
      if( value == null )
        throw new ArgumentNullException( "value" );

      return m_selectedItemsLookup.Contains( value );
    }

    public int IndexOf( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( value == null )
        throw new ArgumentNullException( "value" );

      return m_selectedItems.IndexOf( value );
    }

    public void Insert( int index, object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( ( index < 0 ) || ( index > m_selectedItems.Count ) )
        throw new ArgumentOutOfRangeException( "index is out of range." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null." );

      if( m_selectedItemsLookup.Contains( value ) )
        throw new InvalidOperationException( "Item is already in SelectedItems." );

      using( m_listBox.DeferUpdateSelection() )
      {
        DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;

        if( dataSourceWrapper.IsPrimaryKeyVolatile )
          throw new NotSupportedException( "SelectedItems is read-only for the current data source." );

        DataPath dataPath = dataSourceWrapper.CreateDataPathForObject( value );

        SelectionRange newRange = SelectionManager.SelectionRangeHelper( dataSourceWrapper.DataHelper,
          dataPath,
          true,
          dataPath,
          true,
          SelectionType.Selection );

        m_listBox.SelectedRanges.Add( newRange );
        m_selectedItems.Insert( index, value );
        m_selectedItemsLookup.Add( value );

        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Add, value, index ) );
      }
    }

    public bool IsFixedSize
    {
      get
      {
        return m_listBox.IsAsyncDataSource;
      }
    }

    public bool IsReadOnly
    {
      get
      {
        return m_listBox.IsAsyncDataSource;
      }
    }

    public void Remove( object value )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

      if( value == null )
        throw new ArgumentNullException( "value cannot be null" );

      int removedIndex = m_selectedItems.IndexOf( value );

      if( removedIndex >= 0 )
      {
        this.RemoveAt( removedIndex );
      }
    }

    public void RemoveAt( int index )
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotImplementedException( "SelectedItems is read-only for asynchronous datas sources." );

      if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
        throw new ArgumentOutOfRangeException( "index" );

      using( m_listBox.DeferUpdateSelection() )
      {
        object removedItem = m_selectedItems[ index ];
        m_selectedItems.RemoveAt( index );
        m_selectedItemsLookup.Remove( removedItem );

        DataSourceWrapper dataSourceWrapper = m_listBox.DataSourceWrapper;

        if( dataSourceWrapper.IsPrimaryKeyVolatile )
          throw new NotSupportedException( "SelectedItems is read-only for the current data source." );

        IList<SelectionRange> selectedRanges = m_listBox.SelectedRanges;

        DataPath dataPath = dataSourceWrapper.CreateDataPathForObject( removedItem );

        SelectionRange removedRange = SelectionHelper.FindSelectionRangeForPath( dataSourceWrapper.DataHelper,
          dataPath,
          selectedRanges );

        if( ( removedRange != null )
          && ( removedRange.SelectionType == SelectionType.Selection ) )
        {
          SelectionRange newRange = SelectionManager.SelectionRangeHelper(
                                      dataSourceWrapper.DataHelper,
                                      dataPath,
                                      true,
                                      dataPath,
                                      true,
                                      SelectionType.Unselection );

          selectedRanges.Add( newRange );
        }

        this.RaiseChangedEventsAndUpdateSelectionProperties(
          new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Remove, removedItem, index ) );
      }
    }

    public object this[ int index ]
    {
      get
      {
        // Do not throw for invalid index
        if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
          return null;

        return m_selectedItems[ index ];
      }
      set
      {
        if( m_listBox.IsAsyncDataSource )
          throw new NotImplementedException( "SelectedItems is read-only for asynchronous data sources." );

        if( ( index < 0 ) || ( index >= m_selectedItems.Count ) )
          throw new ArgumentOutOfRangeException( "index" );

        object oldValue = m_selectedItems[ index ];

        if( oldValue != value )
        {
          if( oldValue != null )
          {
            this.Remove( oldValue );
          }

          if( value != null )
          {
            this.Insert( index, value );
          }

          this.RaiseChangedEventsAndUpdateSelectionProperties(
            new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, value, oldValue, index ) );
        }
      }
    }

    #endregion

    #region ICollection Members

    public void CopyTo( Array array, int index )
    {
      ( ( ICollection )m_selectedItems ).CopyTo( array, index );
    }

    public int Count
    {
      get
      {
        return m_selectedItems.Count;
      }
    }

    public bool IsSynchronized
    {
      get
      {
        return ( ( ICollection )m_selectedItems ).IsSynchronized;
      }
    }

    public object SyncRoot
    {
      get
      {
        return ( ( ICollection )m_selectedItems ).SyncRoot;
      }
    }

    #endregion

    #region IEnumerable Members

    public IEnumerator GetEnumerator()
    {
      if( m_listBox.IsAsyncDataSource )
        throw new NotSupportedException( "Use ListBox.[Begin|End]GetSelectedItems." );

      return m_selectedItems.GetEnumerator();
    }

    #endregion
  }
}
